<script setup>
import { ref, watch, onUnmounted, onMounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getPaperAPI, submitPaperAPI } from '@/api/paper.js'
import { formatDate } from '@/utils/format.js'
import { dayjs, ElMessage } from 'element-plus'
import { getFaceInfoAPI, getIPAddressAPI } from '@/api/user'
import * as faceAPI from 'face-api.js'
import { uploadAnswerUserFaceAPI } from '@/api/paper'
import { socket } from '@/utils/socket'
import { useStuStore } from '@/stores'

const route = useRoute()
const router = useRouter()
const paperInfo = ref({})
const answerInfo = ref({
  id: null,
  doTime: null,
  answerItems: []
})
const id = route.params.id
const stuentStore = useStuStore()

// 获取试卷信息
const getPaper = async () => {
  const { data: res } = await getPaperAPI(route.params.id)
  if (res.status === 200) {
    if (res.data.status === 0) {
      ElMessage.warning('试卷已禁用，禁止答题')
      return router.push('/paper')
    }
    const nowDateTime = new Date().toISOString()
    paperInfo.value = res.data
    minute.value = paperInfo.value.suggestTime
    const info = JSON.parse(localStorage.getItem(route.params.id))
    if (!info) {
      answerInfo.value.id = paperInfo.value.id
      for (let i = 0; i < paperInfo.value.titleItems.length; i++) {
        for (
          let j = 0;
          j < paperInfo.value.titleItems[i].questionItem.length;
          j++
        ) {
          const item = {
            id: paperInfo.value.titleItems[i].questionItem[j].id,
            content: '', // 单选、判断、填空、简答答案
            correctArray: [], // 多选题答案
            completed: false, // 是否完成
            itemOrder: paperInfo.value.titleItems[i].questionItem[j].itemOrder // 题目序号
          }
          answerInfo.value.answerItems.push(item)
        }
      }
      localStorage.setItem(route.params.id, JSON.stringify(answerInfo.value))
    } else {
      answerInfo.value = info
    }
    clock(paperInfo.value.suggestTime)
    if (
      paperInfo.value.limitDateTime &&
      paperInfo.value.limitDateTime[0] > nowDateTime
    ) {
      ElMessage.warning('未到答卷时间！')
      return window.close()
    }

    if (
      paperInfo.value.limitDateTime &&
      paperInfo.value.limitDateTime[1] < nowDateTime
    ) {
      ElMessage.error('答卷时间已过！')
      answerInfo.value.doTime = 0
      submitPaperTimeEnd()
    }
  } else {
    ElMessage.error(res.message)
    return router.back()
  }
  await getIPAddressAPI(stuentStore.id)
}
getPaper()

// 计时器
const minute = ref('00')
const second = ref('00')
let n = null
const clock = (time) => {
  const sessionTime = localStorage.getItem(`${id}t`)
  let endTime = sessionTime || time * 60 * 1000

  if (!sessionTime) {
    localStorage.setItem(`${id}t`, sessionTime)
  }
  n = setInterval(() => {
    endTime -= 1000
    localStorage.setItem(id + 't', endTime)
    if (endTime === 0) {
      clearTimeout(n)
      submitPaperTimeEnd() // 提交试卷
      localStorage.removeItem(id + 't')
    }
    minute.value =
      parseInt(endTime / 60 / 1000) < 10
        ? '0' + parseInt(endTime / 60 / 1000)
        : parseInt(endTime / 60 / 1000)
    second.value =
      dayjs(endTime).second() < 10
        ? '0' + dayjs(endTime).second()
        : dayjs(endTime).second()
  }, 1000)
}
watch(
  () => route.params.id,
  () => {
    const info = JSON.parse(localStorage.getItem(route.params.id))
    if (info) {
      answerInfo.value = info.answerInfo
    }
  },
  { immediate: true }
)

// 题目跳转定位
const tagClick = (val) => {
  const p = document.querySelectorAll('p')
  p.forEach((item) => {
    if (+item.getAttribute('data-id') === val) {
      const items = item.offsetTop - 80
      window.scrollTo(0, items)
    }
  })
}

// 答题后修改左侧tag样式
const answerChange = (index) => {
  const answer = answerInfo.value.answerItems[index]
  if (answer.content || answer.correctArray.length > 0) {
    answer.completed = true
  } else {
    answer.completed = false
  }
}

// 时间结束自动提交
const flag = ref(false) // 控制是否弹出提示框
const submitLoading = ref(false) // 提交加载
const submitPaperTimeEnd = async () => {
  answerInfo.value.doTime = paperInfo.value.suggestTime - minute.value
  if (!shoot()) {
    flag.value = false
    return ElMessage.error('未检测到人脸')
  }
  const { data: res } = await submitPaperAPI(answerInfo.value)
  submitLoading.value = true
  if (res.status === 200) {
    const answerUserFace = new FormData()
    answerUserFace.append('faceImage', imgUrl.value)
    answerUserFace.append('id', res.answerId)
    const { data: res2 } = await uploadAnswerUserFaceAPI(answerUserFace)
    if (res2.status !== 200) {
      return ElMessage.error(res2.message)
    }
    ElMessage.success(res.message)
    clearInterval(n)
    localStorage.removeItem(`${answerInfo.value.id}t`)
    localStorage.removeItem(answerInfo.value.id)
    flag.value = true
    socket.emit('paperMonitor', {
      paper_id: id,
      user_id: JSON.parse(localStorage.getItem('uid')),
      link_status: false,
      submit_status: true
    })
    window.close()
    submitLoading.value = false
    return
  }
  ElMessage.error(res.message)
  submitLoading.value = false
}

// 手动提交试卷
const submitPaper = () => {
  for (let item of answerInfo.value.answerItems) {
    if (!item.completed) {
      ElMessageBox.confirm('还有题目未答，是否交卷', '提示', {
        confirmButtonText: '提交',
        cancelButtonText: '取消',
        type: 'warning'
      })
        .then(() => {
          submitPaperTimeEnd()
        })
        .catch(() => {})
      return
    }
  }
  submitPaperTimeEnd()
}

// 监听窗口切换
const switchCount = ref(0)
const handleVisibilitychange = () => {
  if (document.visibilityState === 'hidden') {
    switchCount.value += 1
  }
  if (document.visibilityState === 'visible') {
    if (switchCount.value === 3) {
      submitPaperTimeEnd()
    }
    ElMessage.warning(`当前窗口已经切换${switchCount.value}次`)
  }
}

const handleBeforeUnLoad = () => {
  socket.emit('paperMonitor', {
    paper_id: id,
    user_id: JSON.parse(localStorage.getItem('uid')),
    link_status: false,
    submit_status: false
  })
}
onMounted(() => {
  window.addEventListener('beforeunload', handleBeforeUnLoad)
  document.addEventListener('visibilitychange', handleVisibilitychange)
  initModels()
})

onBeforeUnmount(() => {
  window.removeEventListener('beforeunload', handleBeforeUnLoad)
})
onUnmounted(() => {
  clearInterval(shootTimeout.value)
  shootTimeout.value = null
  localStorage.setItem(id, JSON.stringify(answerInfo.value))
  document.removeEventListener('visibilitychange', handleVisibilitychange)
})

// 人脸监测
const faceInfo = ref(null)
const getFaceInfo = async () => {
  const { data: res } = await getFaceInfoAPI()
  if (res.status === 200) {
    faceInfo.value = res.data
  }
}
getFaceInfo()

const loading = ref(true)
const videoShow = ref(false)

const option = ref({}) // faceAPI模型配置
const initModels = async () => {
  // 初始化faceAPI的模型
  await faceAPI.nets.ssdMobilenetv1.loadFromUri('/models')
  await faceAPI.loadFaceLandmarkModel('/models')
  await faceAPI.nets.faceLandmark68Net.loadFromUri('/models')
  await faceAPI.nets.faceRecognitionNet.loadFromUri('/models')
  option.value = new faceAPI.SsdMobilenetv1Options({
    minConfidence: 0.5
  })
  checkCamera()
}

const imgUrl = ref('')
const videoRef = ref(null)
const canvasRef = ref(null)

// 检测用户是否有摄像头
const stream = ref(null)
const checkCamera = async () => {
  // 获取媒体信息
  const navigator = window.navigator.mediaDevices
  // 判断用户是否有摄像头
  const devices = await navigator.enumerateDevices()
  if (devices) {
    videoShow.value = true // 显示视频画面
    stream.value = await navigator.getUserMedia({
      audio: false,
      video: {
        width: 345,
        height: 204
      }
    })
    try {
      imgUrl.value = ''
      if (videoRef.value) {
        videoRef.value.srcObject = stream.value
        loading.value = false
        videoRef.value.play()
        detectFace()
      }
    } catch (e) {
      //
    }
  }
}

// 人脸识别方法
const timeout = ref(0)
const noOne = ref(null)
const moreThanOne = ref(null)
const shootTimeout = ref(0)
const detectFace = async () => {
  if (videoRef.value.paused) {
    // 判断摄像头是否停止使用
    return clearTimeout(timeout.value)
  }
  const ctx = canvasRef.value.getContext('2d')
  ctx.drawImage(videoRef.value, 0, 0, 345, 204)
  const res = await faceAPI
    .detectAllFaces(canvasRef.value, option.value)
    .withFaceLandmarks()

  if (res.length === 0) {
    if (moreThanOne.value) {
      moreThanOne.value.close()
      moreThanOne.value = null
    }
    !noOne.value && (noOne.value = ElMessage.warning('未识别到人脸'))
  } else if (res.length > 1) {
    if (noOne.value) {
      noOne.value.close()
      noOne.value = null
    }
    !moreThanOne.value &&
      (moreThanOne.value = ElMessage.warning('检测到镜头中存在多人'))
  } else {
    if (noOne.value) {
      noOne.value.close()
      noOne.value = null
    }
    if (moreThanOne.value) {
      moreThanOne.value.close()
      moreThanOne.value = null
    }
  }
  // 绘制人脸框
  res.forEach(({ detection }) => {
    const drawBox = new faceAPI.draw.DrawBox(detection.box, { boxColor: 'red' })
    canvasRef.value && drawBox.draw(canvasRef.value)
  })
  socket.emit('paperMonitor', {
    paper_id: id,
    user_id: JSON.parse(localStorage.getItem('uid')),
    face_info: canvasRef.value.toDataURL('image/png'),
    link_status: true,
    submit_status: false
  })
  requestAnimationFrame(detectFace)
}

// 拍摄,截取摄像头当前帧
const shoot = () => {
  if (noOne.value) {
    ElMessage.warning('未检测到人脸')
    return false
  }
  if (moreThanOne.value) {
    ElMessage.warning('检测到多张人脸')
    return false
  }
  // 将画布内容转为base64并存放
  imgUrl.value = canvasRef.value.toDataURL('image/png')
  // videoRef.value.srcObject.getTracks()[0].stop()
  // videoRef.value.pause()
  return getDescriptors(canvasRef.value.toDataURL('image/png'))
}
const shootInterval = async () => {
  shootTimeout.value = setInterval(shoot, 1000 * 60 * 10)
}
shootInterval()
// 获取图片的人脸描述符，进行下一步操作
const descriptors = ref(null)
const getDescriptors = async (val) => {
  const img = await faceAPI.fetchImage(val)
  const res = await faceAPI
    .detectAllFaces(img, option.value)
    .withFaceLandmarks()
    .withFaceDescriptors()
  descriptors.value = res.map((detection) => detection.descriptor)
  // 进行余弦相似度计算
  return calculateSimilarity(
    descriptors.value[0],
    JSON.parse(faceInfo.value.face_info)
  )
}

const minConfidence = 0.45
const result = ref(false) // 计算结果
// 余弦计算函数
const calculateSimilarity = (newDesc, oldDesc) => {
  oldDesc.length = Object.keys(oldDesc).length
  const res = faceAPI.euclideanDistance(newDesc, oldDesc)

  loading.value = false
  if (res > minConfidence) {
    result.value = false
  } else {
    result.value = true
  }
  return result.value
}
</script>

<template>
  <div class="paperInfo-container">
    <div class="paperInfo-order">
      <el-card shadow="never">
        <template #header>
          <h3 style="font-size: 0.24rem">{{ paperInfo.paperTitle }}</h3>
          <div class="paperInfo-order-header">
            <div class="paperInfo-order-header-deatil">
              <span>
                <i style="color: #989898">总分：</i>
                {{ paperInfo.score }}
              </span>
              <span>
                <i style="color: #989898">时长：</i>
                {{ paperInfo.suggestTime }} 分钟
              </span>
            </div>
            <div class="paperInfo-order-header-createInfo">
              <span>
                <i style="color: #989898">创建人：</i>
                {{ paperInfo.create_user }}
              </span>
              <span>
                <i style="color: #989898">创建时间：</i>
                {{ formatDate(paperInfo.create_time, 'YYYY-MM-DD') }}
              </span>
            </div>
          </div>
        </template>
        <div class="paperInfo-order-suggestTime">
          <span style="color: #989898">剩余时间</span>
          <span class="time">{{ minute }} 分 {{ second }} 秒</span>
        </div>
        <div class="paperInfo-order-titleItem">
          <el-tag
            v-for="(item, index) in paperInfo.question_count"
            :key="index"
            :type="
              answerInfo.answerItems[index]?.completed ? 'success' : 'danger'
            "
            class="items"
            @click="tagClick(item)"
          >
            {{ item }}
          </el-tag>
        </div>
        <div class="paperInfo-order-submit">
          <el-button
            type="primary"
            class="btn"
            @click="submitPaper"
            :loading="submitLoading"
          >
            提交
          </el-button>
        </div>
      </el-card>
      <div
        class="paperInfo-video"
        v-loading="loading"
        element-loading-text="加载中..."
      >
        <video ref="videoRef" style="display: none" />
        <canvas
          width="345"
          height="204"
          ref="canvasRef"
          v-show="videoShow"
        ></canvas>
      </div>
    </div>

    <div class="paperInfo-content">
      <el-card shadow="never">
        <el-scrollbar height="5.56rem">
          <div
            class="paperInfo-content-title"
            v-for="(title, indexs) in paperInfo.titleItems"
            :key="indexs"
          >
            <h3 class="title">{{ title.name }}</h3>
            <div
              class="paperInfo-content-question"
              v-for="question in title.questionItem"
              :key="question.id"
            >
              <p :data-id="question.itemOrder">
                <span>
                  {{ question.itemOrder }}.
                  <span v-html="question.title"></span>
                </span>
              </p>
              <div class="paperInfo-content-question-item">
                <el-radio-group
                  v-if="
                    question.question_type === 1 || question.question_type === 3
                  "
                  v-model="
                    answerInfo.answerItems[question.itemOrder - 1].content
                  "
                >
                  <el-radio-button
                    :label="item.prefix"
                    size="large"
                    v-for="item in question.items"
                    :key="item.prefix"
                    @change="answerChange(question.itemOrder - 1)"
                    >{{
                      `${item.prefix}&nbsp;&nbsp; ${item.content}`
                    }}</el-radio-button
                  >
                </el-radio-group>
                <el-checkbox-group
                  v-else-if="question.question_type === 2"
                  size="large"
                  v-model="
                    answerInfo.answerItems[question.itemOrder - 1].correctArray
                  "
                >
                  <el-checkbox-button
                    :label="item.prefix"
                    v-for="(item, index) in question.items"
                    :key="index"
                    @change="answerChange(question.itemOrder - 1)"
                    >{{
                      `${item.prefix}&nbsp;&nbsp; ${item.content}`
                    }}</el-checkbox-button
                  >
                </el-checkbox-group>
                <el-form
                  v-else-if="question.question_type == 4"
                  :model="
                    answerInfo.answerItems[question.itemOrder - 1].correctArray
                  "
                >
                  <el-form-item
                    v-for="(item, index) in question.items"
                    :key="item.prefix"
                    :label="item.prefix"
                    label-width="1.25rem"
                  >
                    <el-input
                      class="fill-input"
                      placeholder="请填写答案"
                      @change="answerChange(question.itemOrder - 1)"
                      v-model="
                        answerInfo.answerItems[question.itemOrder - 1]
                          .correctArray[index]
                      "
                    ></el-input>
                  </el-form-item>
                </el-form>
                <el-form v-else-if="question.question_type == 5">
                  <el-form-item label=" " label-width="1.25rem">
                    <el-input
                      class="short-answer-textarea"
                      placeholder="请填写答案"
                      type="textarea"
                      resize="none"
                      @change="answerChange(question.itemOrder - 1)"
                      v-model="
                        answerInfo.answerItems[question.itemOrder - 1].content
                      "
                    ></el-input>
                  </el-form-item>
                </el-form>
              </div>
            </div>
          </div>
        </el-scrollbar>
      </el-card>
    </div>
  </div>
</template>

<style lang="scss" scoped>
@media screen and (max-width: 1024px) {
  .paperInfo-container {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 0.4rem !important;
    min-height: 4.8rem;

    .paperInfo-order {
      display: flex;
      flex-direction: row !important;
      margin-bottom: 0.2rem;
      min-width: 3.4rem;
      flex: 1;
      font-size: 0.16rem;
      .el-card {
        margin-right: 0.2rem;
      }
      .paperInfo-order-header {
        font-size: 0.14rem;
        margin-top: 0.2rem;
        display: flex;
        flex-wrap: wrap;
        justify-content: flex-start;

        .paperInfo-order-header-deatil {
          margin-bottom: 0.1rem;
          font-size: 0.14rem;
        }
        .paperInfo-order-header-createInfo {
          font-size: 0.14rem;
        }

        span {
          margin-right: 0.1rem;

          i {
            text-align: right;
          }
        }
      }

      .paperInfo-order-suggestTime {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        padding: 0.2rem 0.4rem;

        .time {
          font-size: 0.28rem;
          margin-top: 0.1rem;
          font-weight: 545;
        }
      }

      .paperInfo-order-titleItem {
        display: flex;
        flex-wrap: wrap;
        justify-content: flex-start;
        margin-bottom: 0.2rem;

        .items {
          width: 0.3rem;
          height: 0.3rem;
          margin-right: 0.1rem;
          margin-bottom: 0.1rem;
          cursor: pointer;
        }
      }

      .paperInfo-order-submit {
        display: flex;
        justify-content: center;
        align-items: center;

        .btn {
          width: 60%;
        }
      }

      .paperInfo-video {
        margin-top: 0.2rem;
        flex: 1;
      }
    }

    .paperInfo-content {
      flex: 4;

      .paperInfo-content-title {
        font-size: 0.16rem;
        .title {
          padding: 0.2rem 0;
          font-size: 0.22rem;
        }

        .paperInfo-content-question {
          padding-left: 0.28rem;
          margin-bottom: 0.2rem;

          p {
            margin-bottom: 0.14rem;

            &:deep(.fill-span) {
              margin: 0 0.06rem;
              padding: 0 0.4rem;
              border-bottom: 0.02rem solid #ccc;
            }
          }

          .paperInfo-content-question-item {
            padding-left: 0.32rem;

            .fill-input,
            .short-answer-textarea {
              width: 40% !important;

              &:deep(.el-textarea__inner) {
                height: 1.2rem;
              }
            }
          }
        }
      }
    }
  }
}

.paperInfo-container {
  display: flex;
  justify-content: space-between;
  padding: 0.3rem 0.8rem;
  min-height: 4.8rem;

  .paperInfo-order {
    display: flex;
    flex-direction: column;
    margin-right: 0.2rem;
    min-width: 3.4rem;
    flex: 1;
    font-size: 0.16rem;
    .paperInfo-order-header {
      font-size: 0.14rem;
      margin-top: 0.2rem;
      display: flex;
      flex-wrap: wrap;
      justify-content: flex-start;

      .paperInfo-order-header-deatil {
        margin-bottom: 0.1rem;
        font-size: 0.14rem;
      }
      .paperInfo-order-header-createInfo {
        font-size: 0.14rem;
      }

      span {
        margin-right: 0.1rem;

        i {
          text-align: right;
        }
      }
    }

    .paperInfo-order-suggestTime {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      padding: 0.2rem 0.4rem;

      .time {
        font-size: 0.28rem;
        margin-top: 0.1rem;
        font-weight: 545;
      }
    }

    .paperInfo-order-titleItem {
      display: flex;
      flex-wrap: wrap;
      justify-content: flex-start;
      margin-bottom: 0.2rem;

      .items {
        width: 0.3rem;
        height: 0.3rem;
        margin-right: 0.1rem;
        margin-bottom: 0.1rem;
        cursor: pointer;
      }
    }

    .paperInfo-order-submit {
      display: flex;
      justify-content: center;
      align-items: center;

      .btn {
        width: 60%;
      }
    }

    .paperInfo-video {
      margin-top: 0.2rem;
      flex: 1;
    }
  }

  .paperInfo-content {
    flex: 4;

    .paperInfo-content-title {
      font-size: 0.16rem;
      .title {
        padding: 0.2rem 0;
        font-size: 0.22rem;
      }

      .paperInfo-content-question {
        padding-left: 0.28rem;
        margin-bottom: 0.2rem;

        p {
          margin-bottom: 0.14rem;

          &:deep(.fill-span) {
            margin: 0 0.06rem;
            padding: 0 0.4rem;
            border-bottom: 0.02rem solid #ccc;
          }
        }

        .paperInfo-content-question-item {
          padding-left: 0.32rem;

          .fill-input,
          .short-answer-textarea {
            width: 40% !important;

            &:deep(.el-textarea__inner) {
              height: 1.2rem;
            }
          }
        }
      }
    }
  }
}
</style>
